package cn.bouncyslime.huffmanTree;

import cn.bouncyslime.huffmanTree.entity.Node;
import cn.bouncyslime.huffmanTree.util.CharacterCounter;
import cn.bouncyslime.huffmanTree.util.HuffmanTree;
import cn.bouncyslime.huffmanTree.util.RuleWriter;
import cn.bouncyslime.huffmanTree.util.TextReader;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * 编码流程
 * <p>
 * 这里还存在一个坑，就是将编码结果01二进制串输出到文件时，因为系统存储的最小单位为字节（8位二进制），
 * 但是生成的赫夫曼编码不一定正好是8的整数倍，所以如果最后一个字节只有一部分是编码结果，比如只有一个0，
 * 那么这个字节的剩余部分就需要用0填充。这时，如果你的编码规则有的为好几个0时，就会出现问题。
 * 因为在解码的结尾可能会多出这么一个或多个字符。
 * <p>
 * 刚才忽然想到一种解决办法：如果有code全为0的叶子节点时，例如000，将其改为00001，
 * 如果001没有节点时，改成001也可以，反正就是对其进行“旋转”。
 * 因为code全为0的叶子节点，肯定特别靠近底层，所以多一层对效率影响应该不大。
 * <p>
 * 上述的解决方法在 {@link HuffmanTree#preventAllZeroString(Node)} 中。
 * <p>
 * 作者： 张一卓
 * 时间： 2019/12/31 11:12
 */
public class Encode {
    /**
     * 编码流程
     *
     * @param filePath 待编码文件的路径
     */
    public void encode(String filePath) {
        // 读取要编码的文件
        String inputText = TextReader.readText(filePath);
        // 对要编码的文件进行字符统计
        Map<Character, Integer> map = CharacterCounter.countCharacter(inputText);
        if (map.isEmpty()) {
            System.out.println("要进行编码文件为空，操作终止！");
            return;
        }

        HuffmanTree huffmanTree = new HuffmanTree();
        // 根据词频统计Map，生成所有用来构造赫夫曼树的Node，并得到List<Node>
        List<Node> list = HuffmanTree.getAllHuffmanNode(map);
        // 生成赫夫曼树，获得树根结点
        Node root = huffmanTree.createTree(list);
        // 将编码规则输出到文件中
        Map<Character, String> codeMap = huffmanTree.getCodeMap();
        System.out.println("以下为各个字符在文章中出现的次数以及该字符的编码规则：");
        map.forEach((k, v) -> System.out.println(k + "字符在文章中出现次数为：" + v + "，该字符的编码规则为：" + codeMap.get(k)));
        RuleWriter.writeRule(codeMap);

        StringBuilder outputText = new StringBuilder();
        // 将文章的编码结果输出到文件中
        for (int i = 0; i < inputText.length(); i++) {
            char character = inputText.charAt(i);
            outputText.append(codeMap.get(character));
        }
        String outputString = outputText.toString();
        writeEncodedResult(outputString);
        // 输出编码前后文件大小对比
        System.out.println("\n编码前文件与编码后文件的对比\n示例文件（编码前文件）为英文文档，其中每个英文字符占据一个字节");
        System.out.println("文件编码前包含字符数量：" + inputText.length() + " 文件编码前大小大约为：" + inputText.length() / 1000 + "kB");
        System.out.println("编码后二进制位数量为：" + outputString.length() + " 编码后文件大小约为：" + outputString.length() / (8 * 1000) + "kB");
    }

    /**
     * 将编码结果的01串输出到文件中
     *
     * @param string 编码结果字符串，二进制01串
     */
    private void writeEncodedResult(String string) {
        byte[] bytes = string.length() % 8 == 0 ? new byte[string.length() / 8] : new byte[string.length() / 8 + 1];
        for (int i = 0; i < string.length(); i++) {
            char character = string.charAt(i);
            if (character == '1') {
                if (i % 8 == 0) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x80);
                } // 1000 0000
                if (i % 8 == 1) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x40);
                } // 0100 0000
                if (i % 8 == 2) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x20);
                } // 0010 0000
                if (i % 8 == 3) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x10);
                } // 0001 0000
                if (i % 8 == 4) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x8);
                } // 0000 1000
                if (i % 8 == 5) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x4);
                } // 0000 0100
                if (i % 8 == 6) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x2);
                } // 0000 0010
                if (i % 8 == 7) {
                    bytes[i / 8] = (byte) ((int) bytes[i / 8] | 0x1);
                } // 0000 0001
            }
        }
        try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("输出文件\\encodedResult"))) {
            outputStream.write(bytes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
